//Copyright 2011 - Jonathan Allen

using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using Granite.ComponentModel;
using System.Diagnostics.Contracts;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace Granite.Modeling.Internals
{
	/// <summary>
	/// Property bag with basic change tracking capabilities.
	/// </summary>
	public class ChangeTrackingPropertyBag : PropertyBagBase
	{

		readonly Dictionary<String, object> m_OriginalValues = new Dictionary<string, object>(StringComparer.Ordinal);
		readonly Dictionary<String, object> m_Values = new Dictionary<string, object>(StringComparer.Ordinal);

		/// <summary>
		/// Property bag with basic change tracking capabilities.
		/// </summary>
		/// <param name="owner"></param>
		public ChangeTrackingPropertyBag(object owner)
			: base(owner)
		{
			Contract.Requires(owner != null);
		}

		/// <summary>
		/// Marks all fields as unchanged by storing them in the original values collection.
		/// </summary>
		public virtual void AcceptChanges(bool recursive)
		{
			m_OriginalValues.Clear();

			foreach (var item in m_Values)
			{
				m_OriginalValues[item.Key] = item.Value;
				if (recursive && item.Value is IChangeTracking)
					((IChangeTracking)item.Value).AcceptChanges();
			}
		}

		/// <summary>
		/// Implmentors need to override this to return the indicated value.
		/// </summary>
		/// <param name="propertyName">Name of the property to fetch.</param>
		/// <returns>The indicated value or System.Reflection.Missing if the value isn't defined.</returns>
		[Pure]
		public override object GetValue([CallerMemberName] string propertyName)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null");
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty.", "propertyName");
			Contract.EndContractBlock();

			if (m_Values.ContainsKey(propertyName))
				return m_Values[propertyName];

			return Missing.Value;
		}

		/// <summary>
		/// This property indicates whether or not the assoicated property was created.
		/// </summary>
		/// <param name="propertyName"></param>
		/// <returns></returns>
		[Pure]
		public override bool IsDefined([CallerMemberName] string propertyName)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null");
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty.", "propertyName");
			Contract.EndContractBlock();

			return m_Values.ContainsKey(propertyName);
		}

		/// <summary>
		/// Replaces the current values collection with the original values collection.
		/// </summary>
		public virtual void RejectChanges(bool recursive)
		{
			//remove properties that no longer exist
			foreach (var item in m_Values.ToList())
			{
				if (!m_OriginalValues.ContainsKey(item.Key))
				{
					m_Values.Remove(item.Key);
					var property = Metadata.Properties[item.Key];
					OnPropertyChanged(property);
					OnRevalidateProperty(property);
				}
			}

			//update remaining properties
			foreach (var item in m_OriginalValues)
			{
				var oldValue = GetValue(item.Key);
				if (!object.Equals(oldValue, item.Value))
				{
					m_Values[item.Key] = item.Value;
					var property = Metadata.Properties[item.Key];
					OnPropertyChanged(property);
					OnRevalidateProperty(property);
				}
			}

			if (recursive)
			{
				foreach (var item in m_Values)
				{
					if (item.Value is IRevertibleChangeTracking)
						((IRevertibleChangeTracking)item.Value).RejectChanges();
				}
			}

			OnRevalidateObject();
		}

		/// <summary>
		/// Implementors need to override this to save the indicated value.
		/// </summary>
		/// <param name="value">The value to be saved. A null will set the value to null. System.Reflection.Missing will delete the field entirely.</param>
		/// <param name="mode">Indicates special handling for the action.</param>
		/// <param name="propertyName">Name of property to update</param>
		/// <returns>True if the value actually changed</returns>
		/// <remarks>This will create the property if it doesn't already exist</remarks>
		public override bool Set(object value, PropertySetModes mode, [CallerMemberName] string propertyName)
		{
			if (propertyName == null)
				throw new ArgumentNullException("propertyName", "propertyName is null");
			if (String.IsNullOrEmpty(propertyName))
				throw new ArgumentException("propertyName is empty.", "propertyName");
			Contract.EndContractBlock();

			var property = Metadata.Properties[propertyName];

			if (mode.HasFlag(PropertySetModes.FixCasing))
				propertyName = property.Name;

			var oldValue = GetValue(propertyName);

			//if (mode.HasFlag(PropertySetModes.ParseType))
			//	value = ParseType((string)value, propertyName);

			if (Object.Equals(oldValue, value))
				return false;

			if (mode.HasFlag(PropertySetModes.SetAsOriginal))
				m_OriginalValues[propertyName] = value;

			m_Values[propertyName] = value;

			if (mode.HasFlag(PropertySetModes.RaiseChangedEvent))
				OnPropertyChanged(property);

			if (mode.HasFlag(PropertySetModes.ValidateProperty))
				OnRevalidateProperty(property);

			if (mode.HasFlag(PropertySetModes.ValidateObject))
				OnRevalidateObject();

			return true;
		}

		/// <summary>
		/// Access to the values dictionary for sub-classes. Extreme care must be taken when working this this dictionary directly, as events will not be automatically fired.
		/// </summary>
		protected Dictionary<String, object> Values
		{
			get { return m_Values; }
		}

		[Pure]
		internal bool IsChangedGraph()
		{
			foreach (var item in m_Values.Values.OfType<IChangeTracking>())
			{
				Contract.Assume(item != null);

				if (item is IChangeTrackingGraph)
				{
					if (((IChangeTrackingGraph)item).IsChangedGraph())
						return true;
				}
				else if (item.IsChanged)
					return true;
			}
			return false;
		}

		[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic"), SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
		[ContractInvariantMethod]
		void ObjectInvariant()
		{
			Contract.Invariant(m_OriginalValues != null);
			Contract.Invariant(m_Values != null);
		}
	}
}

